Skip to main content

Building Docker Images

Docker images are templates used to create Docker containers. Each image contains everything needed to run an application, such as code, dependencies, environment variables, and libraries.

In this section, we'll explore how to:

  • Create a Dockerfile
  • Build custom Docker images
  • Optimize images using layering for efficiency

1. Creating a Dockerfile

A Dockerfile is a script with a series of commands that Docker uses to assemble an image. Think of it as a set of instructions Docker follows to build an environment for your application.

Example 1: Simple Flask Web Application

Let’s walk through an example of creating a Dockerfile for a basic Flask web server.

Step 1: Python Code for the Flask Application

Create a Python file (app.py) for your Flask application:

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
return "Hello, World! Welcome to Docker!"

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

This simple Flask app returns "Hello, World!" when you visit the root URL (/).

Step 2: Define Dependencies in requirements.txt

Create a requirements.txt file to define the Python dependencies. This is useful because it allows Docker to install all dependencies in one go.

flask

Step 3: Writing the Dockerfile

Now, let's create a Dockerfile to containerize this Flask application:

# Use an official Python runtime as the base image
FROM python:3.10-slim

# Set the working directory inside the container
WORKDIR /app

# Copy the requirements file first
COPY requirements.txt /app/

# Install the required Python packages
RUN pip install -r requirements.txt

# Copy the rest of the application code into the container
COPY . /app

# Expose port 5000 to allow access to the Flask app
EXPOSE 5000

# Set the command to run the app
CMD ["python", "app.py"]

Breakdown:

  1. FROM python:3.9-slim: Tells Docker to use a minimal version of Python 3.10 as the base image.
  2. WORKDIR /app: Creates a folder called /app inside the container and sets it as the working directory.
  3. COPY requirements.txt /app/: Copies the requirements.txt file into the container first.
  4. RUN pip install -r requirements.txt: Installs all the dependencies listed in requirements.txt (Flask in this case).
  5. COPY . /app: Copies the rest of the application files (like app.py) into the /app directory in the container.
  6. EXPOSE 5000: Opens port 5000 so the app can be accessed from outside the container.
  7. CMD ["python", "app.py"]: Runs the Flask application inside the container.

Example Directory Structure:

/my-flask-app
├── app.py
├── requirements.txt
├── Dockerfile

Two Ways to Run Your Docker Image

You can run the Docker image using Docker Compose or directly with Docker commands. These are two different methods of managing Docker containers.

1. Running with Docker Compose

Docker Compose simplifies running multi-container Docker applications or managing configurations like port mapping. It uses a docker-compose.yml file to define your services.

Here’s a docker-compose.yml file for this simple Flask app:

version: '3'
services:
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/app

Explanation:

  • build: .: Builds the Docker image from the current directory (where the Dockerfile is located).
  • ports: "5000:5000": Maps port 5000 on the container to port 5000 on your local machine.
  • volumes: .:/app: Mounts the current directory into the /app directory in the container, allowing live updates to the code.

To build and run the app with Docker Compose, use:

docker compose up

This command will:

  • Build the Docker image.
  • Start the container.
  • Map the container port to your local machine's port.

You can then access the Flask app at http://localhost:5000.

2. Running Without Docker Compose

You can also run the application directly with Docker commands, without using Docker Compose. This gives you more manual control but requires you to handle port mapping and volume mounts manually.

To build the image:

docker build -t my-flask-app .

Explanation:

  • -t my-flask-app: Tags the image with the name my-flask-app.
  • .: Tells Docker to use the current directory for the build context (i.e., where the Dockerfile is located).

To run the container:

docker run -p 5000:5000 my-flask-app
  • -p 5000:5000: Maps port 5000 on the container to port 5000 on your local machine.

2. Image Layering and Efficiency

Docker builds images in layers. Each command in the Dockerfile adds a new layer. Docker caches these layers so that if the underlying files haven’t changed, the cached layer is reused, making subsequent builds faster.

Example 2: Optimizing Layer Caching

Let’s optimize the previous Dockerfile by reducing unnecessary layer rebuilds.

# Use a minimal Python base image
FROM python:3.9-slim

# Set the working directory
WORKDIR /app

# Copy the requirements file first to leverage layer caching
COPY requirements.txt /app/

# Install dependencies
RUN pip install -r requirements.txt

# Now copy the rest of the application code
COPY . /app

# Expose port 5000
EXPOSE 5000

# Start the Flask app
CMD ["python", "app.py"]

Explanation:

  • COPY requirements.txt: We copy the requirements.txt file first and install the dependencies before copying the rest of the application. This ensures that even if you make small changes to the code, Docker doesn’t need to reinstall all the dependencies unless they change.

Docker Compose File

version: '3'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"
volumes:
- .:/app

3. Multi-Stage Builds for Efficiency

In more complex applications, you might want to separate the build process from the runtime environment. This reduces the size of the final image by only including the necessary runtime components, without build dependencies.

Example 3: Multi-Stage Build

# Stage 1: Build the app
FROM python:3.9-slim AS builder

WORKDIR /app
COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app

# Stage 2: Create the final lightweight image
FROM python:3.9-slim

WORKDIR /app
COPY --from=builder /app /app

EXPOSE 5000
CMD ["python", "app.py"]

Explanation:

  • Stage 1 (Builder): The application and its dependencies are installed here.
  • Stage 2 (Final Image): We copy only the necessary files from the builder stage into a new, clean image, which is much smaller and faster to run.

Docker Compose File

version: '3'
services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- "5000:5000"

Summary

  • Dockerfiles define how to build Docker images layer by layer.
  • You can run Docker containers either with Docker Compose (which simplifies managing multi-container applications) or without Docker Compose (using direct Docker commands).
  • Layering improves build efficiency by caching unchanged parts of the image.
  • Multi-stage builds allow for smaller images by separating the build process from the runtime environment.